package com.livestocksolutions;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;

import com.livestocksolutions.util.UpdateHistory;

/* -- Version History --
 *  1.0 : 11 July 2012 - Initial version.
 *  
 *  1.1 : 11 July 2012 - Change Float to double, to allow for the greater possible precision values
 *
 *  1.2 : 16 July 2012 - Class now implements Serializable
 * 
 *  1.3 : 05 September 2012 - Added private long modifiedTime, private boolean modified and private final long id
 *                       public long getID(), public boolean getModified() and public long getModifiedDate().
 *                       Changed private Location storageLocation to private long storageLocation, private Owner
 *                       origin to private long origin, and associated methods setStorageLocation(Location),
 *                       setOrigin(Business), getStorageLocation() and getOrigin(). Changed return type to
 *                       setAmountUsed(double) to boolean from void to indicate if too much is trying to be removed
 *                       from the quantity.
 *                       
 *  1.4 : 24 September 2012 - Added Update methods that will allow the comparing and updation of a similar Object.
 *                       Serialisation method no longer requires UsableItem to implement Serializable.
 *                    
 *  1.5 : 13 October 2012 - Added method boolean checkCanUse(double,String) to check if that amount can be used of
 *                       this Item. Added method double convertToSameUnits(double, String) to return a value in the
 *                       same measurements used in the UsableItem. Changed double cost visibility to protected.
 *  
 *  2.0 : 27 December 2012 - Remake of class to have objects more as templates than actual real references.
 *  
 *  2.1 : 06 March 2013 - Class is now a generic class that supplies a subclass of UsedItem.
 */

/**
 * This class provides a base for all items that are usable within the Farm
 * @author Adam Parr
 * @version 2.1
 * 
 * @.modified 06 March 2013
 * 
 * @param <T> A sub class of UsedItem that the UsableItem accepts as a Mixture.
 */
public abstract class UsableItem<T extends UsedItem>{
	/**
	 * Keep track of any changes of this UsableItem.
	 */
	protected HashMap<String,UpdateHistory> update = new HashMap<String,UpdateHistory>();
	
	/**
	 * All Ts that are used to create this UsableItem.<br>
	 * An empty list indicates this <tt>T</tt> is not made of a mixture.
	 */
	protected ArrayList<T> mixture = new ArrayList<T>();
	/**
	 * Unique ID that identifies this UsableItem
	 */
	protected final long id;
	/**
	 * Name of this item.
	 */
	private String name;
	/**
	 * A short description describing this item.
	 */
	private String description;
	/**
	 * When the UsableItem is created using a mixture of other items, the ratios
	 * of all mixtures used to create this item will create this amount. If the
	 * UsableItem is not a mixture, then quantity and measurement will have no
	 * meaning.
	 */
	private double quantity;
	/**
	 * Measurement type provided in the quantity.
	 * such as: ml, L, gram, KG, and so on.
	 */
	private String measurement;
	
	/**
	 * Constructor for creating an UsableItem
	 */
	public UsableItem(){
		long tempID = Calendar.getInstance().getTimeInMillis();
		while(LivestockSolutions.getUsableItemByID(tempID)!=null){
			// Just in case multiple items of this type are made in the same millisecond
			tempID++;
		}
		id=tempID;
	}
	
	/**
	 * Get the name of this item
	 * @return the name of this item
	 */
	public String getName(){
		return name;
	}
	
	/**
	 * Get the location of where this item is stored
	 * @return location this item is stored
	 * @deprecated No longer used
	 */
	public Location getStorageLocation(){
		return null;
		//return LivestockSolutions.getLocationByID(storageLocation);
	}
	
	/**
	 * Get the description of this item
	 * @return description of this item
	 */
	public String getDescription(){
		return description;
	}
	
	/**
	 * Get the quantity of this item
	 * @return quantity of this item
	 */
	public double getQuantity(){
		return quantity;
	}
	
	/**
	 * Get the measurement that quantity is in
	 * @return measurement that quantity is in
	 */
	public String getMeasurement(){
		return measurement;
	}
	
	/**
	 * Get the date this item were purchased
	 * @return purchase date of the item
	 * @deprecated A purchase date is no longer used for any particular item {@link UsableItemHistory}
	 */
	public Date getPurchaseDate(){
		return null;// purchaseDate==0?null:new Date(purchaseDate);
	}
	
	/**
	 * Set the name of this item.<br>
	 * @param name name to set this item.
	 */
	public void setName(String name){
		if(name!=null&&!name.trim().equals("")&&!name.equals(this.name)){
			this.name=name;
			update.put("NAME", new UpdateHistory(this.name));
		}
	}
	
	/**
	 * Set the Storage Location of this item.
	 * @param storageLocation location of where this item is stored
	 * @deprecated storageLocation is no longer used.
	 */
	public void setStorageLocation(Location storageLocation){
		//if(storageLocation==null||this.storageLocation!=storageLocation.getID()){
		//	this.storageLocation=storageLocation!=null?storageLocation.getID():0;
		//	update.put("STORAGE", new UpdateHistory(String.valueOf(this.storageLocation)));
		//}
	}
	
	/**
	 * Set a description of this item.
	 * @param description a description that describes this item
	 */
	public void setDescription(String description){
		if(this.description!=description){
			this.description=description;
			update.put("DESCRIPTION", new UpdateHistory(this.description));
		}
	}
	
	/**
	 * Set the quantity of the amount produced in a standard mixture.
	 * @param quantity amount of this item purchased
	 */
	public void setQuantity(double quantity){
		if(quantity!=this.quantity){
			this.quantity=quantity;
			update.put("QUANTITY", new UpdateHistory(String.valueOf(this.quantity)));
		}
	}
	
	/**
	 * Set the measurement unit of quantity and amount used.
	 * @param measurement unit that quantity and amount used
	 */
	public void setMeasurement(String measurement){
		if(this.measurement!=measurement){
			this.measurement=measurement;
			update.put("MEASUREMENT", new UpdateHistory(this.measurement));
		}
	}
	
	/**
	 * Set the purchase date of this item.
	 * @param purchaseDate of this item
	 * @deprecated A purchase date is no longer used for any particular item {@link UsableItemHistory}
	 */
	public void setPurchaseDate(Date purchaseDate){
		//if(this.purchaseDate==0&&purchaseDate!=null){
		//	this.purchaseDate=purchaseDate.getTime();
		//	update.put("PURCHASEDATE", new UpdateHistory(String.valueOf(this.purchaseDate)));
		//}
	}
	
	/**
	 * Set the amount used of this item to the given value
	 * @param amountUsed the amount of this item already used
	 * @return <tt>true</tt> if the total amount used is less than or equal to
	 *           the quantity of this item. <tt>false</tt> is returned if adding
	 *           this amount will take it over.
	 * @deprecated No longer used
	 */
	public boolean setAmountUsed(double amountUsed){
		//if(this.amountUsed+amountUsed<=quantity){
		//	this.amountUsed=this.amountUsed+amountUsed;
		//	update.put("AMOUNTUSED", new UpdateHistory(String.valueOf(this.amountUsed)));
		//	return true;
		//}
		return false;
	}
	
	/**
	 * Get the unique id of this UsableItem
	 * @return the id of this UsableItem
	 */
	public long getID(){
		return id;
	}
	
	/**
	 * Checks to see if the quantity given is less than or equal to the amount of this quantity left
	 * @param quantity The amount to check to see if that amount can be used.
	 * @param measurement The String that indicates what units the quantity is in (ml, l, mg, g, kg, ...)
	 * @return <tt>true</tt> if the quantity able to be used, <tt>false</tt> when the quantity can not be
	 *          determined or exceeds the quantity remaining of the UsableItem.
	 * 
	 * @deprecated No longer used
	 */
	public boolean checkCanUse(double quantity, String measurement){
	//	double value = convertToSameUnits(quantity, measurement);
	//	return value<=this.quantity-amountUsed && value>=0;
		return true;
	}
	
	/**
	 * Convert the quantity given to the same units used in this UsableItem.
	 * @param quantity The amount to check to see if that amount can be used.
	 * @param measurement The String that indicates what units the quantity is in (ml, l, mg, g, kg, ...)
	 * @return The units converted to the same measurement used in this UsableItem, or -1 if there is a problem.
	 */
	public double convertToSameUnits(double quantity, String measurement){
		measurement=measurement.toLowerCase();
		String thisMeasurement=getMeasurement().toLowerCase();
		if(!thisMeasurement.equals(measurement)){
			if(measurement.equals("ml")||thisMeasurement.equals("l")){
				// Volume
				if(measurement.equals("ml") && thisMeasurement.equals("l")){
					quantity = quantity/1000;
					measurement="l";
				}else if(measurement.equals("l") && thisMeasurement.equals("ml")){
					quantity = quantity*1000;	
					measurement="ml";
				}else{
					return -1;
				}
			}else if(measurement.equals("mg")|| measurement.equals("g")|| measurement.equals("kg")){
				// Weights
				if(measurement.equals("mg") &&(thisMeasurement.equals("g") || thisMeasurement.equals("kg"))){
					quantity = quantity/1000;
					measurement="g";
				}else if(measurement.equals("kg") &&(thisMeasurement.equals("mg") || thisMeasurement.equals("g"))){
					quantity = quantity*1000;
					measurement="g";
				}
				if(measurement.equals("g") && thisMeasurement.equals("kg")){
					quantity = quantity/1000;
					measurement="kg";
				}else if(measurement.equals("g") &&thisMeasurement.equals("mg")){
					quantity = quantity*1000;
					measurement="mg";
				}else if(!measurement.equals("g")){
					return -1;
				}
			}
		}
		return quantity;
	}
	
	/**
	 * Get all feeds that were added together to produce this UsableItem.
	 * @return An array of <tt>T</tt> that detail the ratio and UsableItem.
	 */
	@SuppressWarnings("unchecked")
	public T[] getIncludedMixtures(){
		T[] t =getArray();
		for(int x=0; x<mixture.size();x++){
			// The Objects get stored in mixture as Generic class UsedItem objects, change these Objects to reflect what <tt>T</tt> is.
			t[x]=(T)mixture.get(x).changeType((Class<? extends UsedItem>) t.getClass().getComponentType());
		}
		return t;
	}
	
//	/**
//	 * Adds an UsableItem to this UsableItem.
//	 * @param addedItem The Item that makes up the composition of this item.
//	 */
//	public void addIncludedMixture(UsedItem addedItem){
//		boolean exitst = false;
//		for(UsedItem ui : mixture){
//			if(ui.getID()==addedItem.getID()){
//				exitst=true;
//				break;
//			}
//		}
//		if(!exitst){
//			mixture.add(addedItem);
//			
//			UpdateHistory mixtureList = update.get("MIXTURE")!=null? update.get("MIXTURE"):new UpdateHistory();
//			mixtureList.setValues(String.valueOf(addedItem.getID()),new UpdateHistory("true"));
//			update.put("MIXTURE", mixtureList);
//		}
//	}
	
	/**
	 * Adds a Feed to this UsableItem.
	 * @param addedItem The history of use of the item to add.
	 * @return <tt>true</tt> if the addedItem was added. <tt>false</tt> otherwise.
	 */
	public boolean addIncludedMixture(T addedItem){
		for(UsedItem ui : mixture){
			if(ui.getID()==addedItem.getID()){
				return false;
			}
		}
		mixture.add(addedItem);
		
		UpdateHistory mixtureList = update.get("MIXTURE")!=null? update.get("MIXTURE"):new UpdateHistory();
		mixtureList.setValues(String.valueOf(addedItem.getID()),new UpdateHistory("true"));
		update.put("MIXTURE", mixtureList);
		return true;
	}
	
	/**
	 * Remove the specified item from the list of mixtures for this item.
	 * @param item The Item to remove from being a component of this mixture.
	 * @return <tt>true</tt> If the item was removed, <tt>false</tt> otherwise.
	 */
	public boolean removeIncludedMixture(T item){
		for(UsedItem ui : mixture){
			if(ui==null || ui.getID()== item.getID()){
				mixture.remove(ui);
				
				if(ui!=null){
					UpdateHistory mixtureList = update.get("MIXTURE")!=null? update.get("MIXTURE"):new UpdateHistory();
					mixtureList.setValues(String.valueOf(item.getID()),new UpdateHistory("false"));
					update.put("MIXTURE", mixtureList);
				}
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Updates this Object with the updated values from the different version.
	 * @param usableItem UsableItem that represents this Object in a different version.
	 * @return <tt>true</tt>.
	 */
	public boolean update(UsableItem<? extends UsedItem> usableItem){
		if(usableItem!=null && this!=usableItem){
			if(updateBefore("NAME",usableItem)){
				this.name=usableItem.update.get("NAME").getValue();
				update.put("NAME", new UpdateHistory(this.name));
			}
		//	if(updateBefore("STORAGE",changes)){
		//		this.storageLocation=Long.valueOf(changes.update.get("STORAGE").getValue());
		//		update.put("STORAGE", new UpdateHistory(String.valueOf(this.storageLocation)));
		//	}
			if(updateBefore("DESCRIPTION",usableItem)){
				this.description=usableItem.update.get("DESCRIPTION").getValue();
				update.put("DESCRIPTION", new UpdateHistory(this.description));
			}
			if(updateBefore("QUANTITY",usableItem)){
				this.quantity=Double.valueOf(usableItem.update.get("QUANTITY").getValue());	
				update.put("QUANTITY", new UpdateHistory(String.valueOf(this.quantity)));
			}
			if(updateBefore("MEASUREMENT",usableItem)){
				this.measurement=usableItem.update.get("MEASUREMENT").getValue();
				update.put("MEASUREMENT", new UpdateHistory(this.measurement));
			}
		//	if(updateBefore("PURCHASEDATE",changes)){
		//		this.purchaseDate=Long.valueOf(changes.update.get("PURCHASEDATE").getValue());
		//		update.put("PURCHASEDATE", new UpdateHistory(String.valueOf(this.purchaseDate)));
		//	}

		//	if(updateBefore("AMOUNTUSED",changes)){
		//		this.amountUsed=Double.valueOf(changes.update.get("AMOUNTUSED").getValue());
		//		update.put("AMOUNTUSED", new UpdateHistory(String.valueOf(this.amountUsed)));
		//	}
			
			if(usableItem!=null && this!=usableItem){
				if(usableItem.update.get("MIXTURE")!=null){
					// Create a hashmap linking current IDs to their associated Objects.
					HashMap<Long,UsedItem> map = new HashMap<Long,UsedItem>();
					for(UsedItem ui : mixture){
						map.put(ui.getID(),ui);
					}
					
					for(String key : usableItem.update.get("MIXTURE").toKeyArray()){
						if(update.get("MIXTURE")==null || update.get("MIXTURE").getValue(key)==null || !update.get("MIXTURE").getValue(key).getDate().after(usableItem.update.get("MIXTURE").getValue(key).getDate())){
							UpdateHistory mixtureList = update.get("MIXTURE")!=null?update.get("MIXTURE"):new UpdateHistory();
							UsedItem item=null;
		
							if((item=map.get(Long.parseLong(key)))!=null){
								if(usableItem.update.get("MIXTURE").getValue(key).equals("false")){
									// Remove
									mixture.remove(item);
									mixtureList.setValues(key,new UpdateHistory("false"));
								}else{
									// Add
									for(UsedItem u:usableItem.mixture){
										if(item.getID()==u.getID()){
											item.update(u);
											mixtureList.setValues(key,new UpdateHistory("true"));
											break;
										}
									}
								}
							}else if(usableItem.update.get("MIXTURE").getValue(key).equals("true")){
								for(UsedItem u:usableItem.mixture){
									if(u.getID()==Long.parseLong(key)){
										mixture.add((T)u);
										mixtureList.setValues(key,new UpdateHistory("true"));
										break;
									}
								}
							}
							update.put("MIXTURE", mixtureList);
						}
					}
				}
			}
			return true;
		}
		return false;
	}
	
	/**
	 * Checks to see if the modification date of the item at this position is before the changed value
	 * @param key The String key at which to compare the modification dates of the modified objects.
	 * @param changes The UsableItem that represents this UsableItem on another system.
	 * @return <tt>true</tt> if the old value was modified before the changed value. <tt>false</tt> otherwise.
	 */
	protected boolean updateBefore(String key,UsableItem<? extends UsedItem> changes){
		if(this.update.get(key)==null && changes.update.get(key)!=null){
			return true;
		}else if(changes.update.get(key)==null){
			return false;
		}
		return !this.update.get(key).getDate().after(changes.update.get(key).getDate());
	}
	
	/**
	 * Get an empty Array of <tt>T</tt> with enough spaces to hold all elements in {@link #mixture}.
	 * @return an empty array of <tt>T</tt>.
	 */
	protected abstract T[] getArray();
}
